// Package article
/**
* @Project : geektime-basic-go-study
* @File    : article.go
* @IDE     : GoLand
* @Author  : Tvux
* @Date    : 2024/11/9 09:54
**/

package service

import (
	"context"
	"geektime-basic-go-study/webook/backend/internal/domain"
	"geektime-basic-go-study/webook/backend/internal/repository/article"
	"github.com/HJH0924/GenericGo/logger"
	"time"
)

type ArticleService interface {
	CreateOrUpdate(ctx context.Context, article domain.Article) (int64, error)
	Publish(ctx context.Context, article domain.Article) (int64, error)
	PublishV1(ctx context.Context, article domain.Article) (int64, error)
}

var _ ArticleService = (*ArticleServiceImpl)(nil)

type ArticleServiceImpl struct {
	logger.Logger
	// V0
	repo article.ArticleRepository

	// V1
	authorRepo article.ArticleAuthorRepository
	readerRepo article.ArticleReaderRepository
}

func (Self *ArticleServiceImpl) Publish(ctx context.Context, article domain.Article) (int64, error) {
	return Self.repo.Sync(ctx, article)
}

func (Self *ArticleServiceImpl) PublishV1(ctx context.Context, article domain.Article) (int64, error) {
	// 下面这样写有问题，假设是我的文章一直都在制作库里面，而没有发表过，
	// 此时我进行修改然后发表，那么制作库的文章会更新成功，但是线上库的文章会更新失败
	// 所以线上库应该有个 upsert 的语义，即有就更新，没有就新建
	// 那线上库是否需要先查一下？有就更新，没有就新建 - 答案是不行，会有并发问题 典型的 CheckDoSomething 场景
	//if article.Id > 0 {
	//	err := Self.authorRepo.UpdateByIdAndAuthorId(ctx, article)
	//	if err != nil {
	//		return article.Id, err
	//	}
	//	err = Self.readerRepo.UpdateByIdAndAuthorId(ctx, article)
	//	return article.Id, err
	//}
	//aid, err := Self.authorRepo.Create(ctx, article)
	//if err != nil {
	//	return 0, err
	//}
	//// 确保线上库的文章id与制作库的相同
	//article.Id = aid
	//return Self.readerRepo.Create(ctx, article)

	var (
		id  = article.Id
		err error
	)
	if id > 0 {
		err = Self.authorRepo.UpdateByIdAndAuthorId(ctx, article)
	} else {
		id, err = Self.authorRepo.Create(ctx, article)
	}
	if err != nil {
		return 0, err
	}
	// 确保线上库的文章id与制作库的相同
	article.Id = id
	const maxRetries = 3
	for i := 0; i < maxRetries; i++ {
		time.Sleep(time.Second * time.Duration(i+1)) // 简单的指数退避
		id, err = Self.readerRepo.Save(ctx, article)
		if err == nil {
			return id, err
		}
		Self.Debug("保存到线上库失败，正在重试",
			logger.Field{Key: "article.Id", Val: article.Id},
			logger.Field{Key: "retry_count", Val: i + 1},
			logger.ErrorField(err),
		)
	}
	Self.Debug("重试彻底失败，无法保存到线上库",
		logger.Field{Key: "article.Id", Val: article.Id},
		logger.ErrorField(err),
	)
	// 接入你的告警系统，手工处理一下
	// 走异步，我直接保存到本地文件
	// 走 Canal
	// 打 MQ
	return id, err
}

func NewArticleService(repo article.ArticleRepository) ArticleService {
	return &ArticleServiceImpl{
		repo: repo,
	}
}

func NewArticleServiceV1(zl logger.Logger, authorRepo article.ArticleAuthorRepository, readerRepo article.ArticleReaderRepository) ArticleService {
	return &ArticleServiceImpl{
		Logger:     zl,
		authorRepo: authorRepo,
		readerRepo: readerRepo,
	}
}

func (Self *ArticleServiceImpl) CreateOrUpdate(ctx context.Context, article domain.Article) (int64, error) {
	if article.Id > 0 {
		// 更新
		// 现在要加入逻辑，不允许更新别人的帖子
		// 首先想到的就是在这里先查一下数据库，看对应的文章作者跟传进来的是否一致，如果不一致就返回
		// 这样写可以，但是性能比较差，先查一次数据库，如果一致的话再更新，又访问一次数据库，但是因为一般不会有这种情况出现，大部分用户都是操作自己的文章，所以性能也可以接受
		// 但是有更好的写法，即修改 dao 层的 SQL 查询条件
		err := Self.repo.UpdateByIdAndAuthorId(ctx, article)
		return article.Id, err
	}
	return Self.repo.Create(ctx, article)
}
